Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / ble / src / commonMain / kotlin / org / meshtastic / core / ble / KableBleScanner.kt

Displaying Raw • Download

core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleScanner.kt 2d20cd8a4708e2ef66e98d5259f3a97ec93f240a (2d20cd8a) Text, 8.37 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.ble

Tff7b72import T7ee787com.juul.kable.Advertisement
Tff7b72import T7ee787com.juul.kable.Scanner
Tff7b72import T7ee787com.juul.kable.UnmetRequirementException
Tff7b72import T7ee787com.juul.kable.UnmetRequirementReason
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.flow.Flow
Tff7b72import T7ee787kotlinx.coroutines.flow.channelFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.collect
Tff7b72import T7ee787kotlinx.coroutines.flow.map
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787kotlin.time.Duration
Tff7b72import T7ee787kotlin.uuid.Uuid

Tff7b72private Tff7b72const Tff7b72val Te6edf3MAX_SCAN_START_FAILURE_CAUSE_DEPTH Tff7b72= T79c0ff1T79c0ff0

Tff7b72internal Tff7b72sealed Tff7b72interface T56d364KableScanFilter Tb4b4b4{
Tff7b72data Tff7b72object T56d364None Tb4b4b4: Te6edf3KableScanFilter

Tff7b72data Tff7b72class T56d364AddressTb4b4b4(Tff7b72val Te6edf3valueTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4: Te6edf3KableScanFilter

Tff7b72data Tff7b72class T56d364ServiceUuidTb4b4b4(Tff7b72val Te6edf3valueTb4b4b4: Te6edf3UuidTb4b4b4) Tb4b4b4: Te6edf3KableScanFilter
Tb4b4b4}

Tff7b72internal Tff7b72data Tff7b72class T56d364KableScanResultTb4b4b4(Tff7b72val Te6edf3identifierTb4b4b4: Tffa657StringTb4b4b4, Tff7b72val Te6edf3nameTb4b4b4: Tffa657String?Tb4b4b4, Tff7b72val Te6edf3advertisementTb4b4b4: Te6edf3Advertisement?Tb4b4b4)

Tff7b72internal Tff7b72fun Td2a8ffresolveKableScanFilterTb4b4b4(Te6edf3serviceUuidTb4b4b4: Te6edf3Uuid?Tb4b4b4, Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Te6edf3KableScanFilter Tff7b72= Tff7b72when Tb4b4b4{
Te6edf3address Tff7b72!Tff7b72= Tff7b72null Tff7b72-Tff7b72> Te6edf3KableScanFilterTb4b4b4.Te6edf3AddressTb4b4b4(Te6edf3addressTb4b4b4)
Te6edf3serviceUuid Tff7b72!Tff7b72= Tff7b72null Tff7b72-Tff7b72> Te6edf3KableScanFilterTb4b4b4.Te6edf3ServiceUuidTb4b4b4(Te6edf3serviceUuidTb4b4b4)
Tff7b72else Tff7b72-Tff7b72> Te6edf3KableScanFilterTb4b4b4.Te6edf3None
Tb4b4b4}

T8b949e// Kable's Advertisement.identifier is an expect typealias: String on Android/JVM/JS but Uuid on Apple.
T8b949e// toString() looks redundant when compiling the Android/JVM view (hence the warning) but is required to
T8b949e// normalize the Apple Uuid to the String this result carries, so it must stay.
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffREDUNDANT_CALL_OF_CONVERSION_METHODTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Te6edf3AdvertisementTb4b4b4.Td2a8fftoScanResultTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3KableScanResult Tff7b72=
Te6edf3KableScanResultTb4b4b4(Te6edf3identifier Tff7b72= Te6edf3identifierTb4b4b4.Te6edf3toStringTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3name Tff7b72= Te6edf3nameTb4b4b4, Te6edf3advertisement Tff7b72= Tff7b72thisTb4b4b4)

Tf0883e@SingleTb4b4b4(Te6edf3binds Tff7b72= Tff7b72[Te6edf3BleScannerTff7b72::Te6edf3classTff7b72]Tb4b4b4)
Tff7b72open Tff7b72class T56d364KableBleScannerTb4b4b4(Tff7b72private Tff7b72val Te6edf3loggingConfigTb4b4b4: Te6edf3BleLoggingConfigTb4b4b4) Tb4b4b4: Te6edf3BleScanner Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3scanStartLimiter Tff7b72= Te6edf3createBleScanStartLimiterTb4b4b4(Tb4b4b4)

Tff7b72internal Tff7b72open Tff7b72suspend Tff7b72fun Td2a8ffreserveScanStartTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3scanStartLimiterTb4b4b4.Te6edf3reserveStartTb4b4b4(Tb4b4b4)

Tff7b72internal Tff7b72open Tff7b72fun Td2a8ffadvertisementsTb4b4b4(Te6edf3filterTb4b4b4: Te6edf3KableScanFilterTb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3KableScanResultTff7b72> Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3Scanner Tb4b4b4{
Te6edf3logging Tb4b4b4{ Te6edf3applyConfigTb4b4b4(Te6edf3loggingConfigTb4b4b4) Tb4b4b4}
Tff7b72when Tb4b4b4(Te6edf3filterTb4b4b4) Tb4b4b4{
Te6edf3KableScanFilterTb4b4b4.Te6edf3None Tff7b72-Tff7b72> Tffa657Unit
Tff7b72is Te6edf3KableScanFilterTb4b4b4.Te6edf3Address Tff7b72-Tff7b72> Te6edf3filters Tb4b4b4{ Te6edf3match Tb4b4b4{ Te6edf3address Tff7b72= Te6edf3filterTb4b4b4.Te6edf3value Tb4b4b4} Tb4b4b4}
Tff7b72is Te6edf3KableScanFilterTb4b4b4.Te6edf3ServiceUuid Tff7b72-Tff7b72> Te6edf3filters Tb4b4b4{ Te6edf3match Tb4b4b4{ Te6edf3services Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3filterTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72return Te6edf3scannerTb4b4b4.Te6edf3advertisementsTb4b4b4.Te6edf3mapTb4b4b4(Te6edf3AdvertisementTff7b72::Te6edf3toScanResultTb4b4b4)
Tb4b4b4}

T8b949e// ThrowsCount: three deliberate rethrow paths, one per exception family Kable can surface here — cancellation
T8b949e// (must propagate untouched), UnmetRequirementException (an IOException) and IllegalStateException. They have no
T8b949e// common supertype below Exception, so merging them would mean catching Exception broadly instead.
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffThrowsCountTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffscanTb4b4b4(Te6edf3timeoutTb4b4b4: Te6edf3DurationTb4b4b4, Te6edf3serviceUuidTb4b4b4: Te6edf3Uuid?Tb4b4b4, Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3BleDeviceTff7b72> Tb4b4b4{
Tff7b72val Te6edf3filter Tff7b72= Te6edf3resolveKableScanFilterTb4b4b4(Te6edf3serviceUuid Tff7b72= Te6edf3serviceUuidTb4b4b4, Te6edf3address Tff7b72= Te6edf3addressTb4b4b4)

T8b949e// Kable's Scanner doesn't enforce timeout internally, it runs until the Flow is cancelled.
T8b949e// By wrapping it in a channelFlow with a timeout, we enforce the BleScanner contract cleanly.
Tff7b72return Te6edf3channelFlow Tb4b4b4{
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3timeoutTb4b4b4) Tb4b4b4{
Te6edf3reserveScanStartTb4b4b4(Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3advertisementsTb4b4b4(Te6edf3filterTb4b4b4)Tb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3advertisement Tff7b72-Tff7b72>
Te6edf3sendTb4b4b4(
Te6edf3MeshtasticBleDeviceTb4b4b4(
Te6edf3address Tff7b72= Te6edf3advertisementTb4b4b4.Te6edf3identifierTb4b4b4,
Te6edf3name Tff7b72= Te6edf3advertisementTb4b4b4.Te6edf3nameTb4b4b4,
Te6edf3advertisement Tff7b72= Te6edf3advertisementTb4b4b4.Te6edf3advertisementTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3exTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3ex
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3exTb4b4b4: Te6edf3UnmetRequirementExceptionTb4b4b4) Tb4b4b4{
T8b949e// Kable models "Bluetooth is off" and "location services are off" as an IOException, so these
T8b949e// never matched the IllegalStateException branch below and escaped raw to ViewModel-level
T8b949e// catch-alls that log at error — the top source of Crashlytics/RUM noise. Map them to the
T8b949e// typed, non-reported BleScanStartException instead.
Tff7b72throw Te6edf3exTb4b4b4.Te6edf3asBleScanStartExceptionTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3exTb4b4b4: Te6edf3IllegalStateExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3exTb4b4b4.Te6edf3asBleScanStartExceptionOrNullTb4b4b4(Tb4b4b4) Tff7b72?: Te6edf3ex
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Maps Kable's typed [UnmetRequirementReason] onto the matching [BleScanStartFailureReason].
*
* Kable's reason enum is exhaustive over the preconditions it checks, so this needs no message matching — unlike the
* [IllegalStateException] paths below, which Kable only distinguishes by message text.
*
* Kept `internal` and separate from the exception so it stays unit-testable: [UnmetRequirementException] has an
* `internal` constructor in Kable and cannot be instantiated from our tests.
*/
Tff7b72internal Tff7b72fun Te6edf3UnmetRequirementReasonTb4b4b4.Td2a8fftoBleScanStartFailureReasonTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleScanStartFailureReason Tff7b72= Tff7b72when Tb4b4b4(Tff7b72thisTb4b4b4) Tb4b4b4{
Te6edf3UnmetRequirementReasonTb4b4b4.Te6edf3BluetoothDisabled Tff7b72-Tff7b72> Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3BluetoothDisabled
Te6edf3UnmetRequirementReasonTb4b4b4.Te6edf3LocationServicesDisabled Tff7b72-Tff7b72> Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3LocationServicesDisabled
Tb4b4b4}

Tff7b72private Tff7b72fun Te6edf3UnmetRequirementExceptionTb4b4b4.Td2a8ffasBleScanStartExceptionTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleScanStartException Tff7b72=
Te6edf3BleScanStartExceptionTb4b4b4(Te6edf3reasonTb4b4b4.Te6edf3toBleScanStartFailureReasonTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72thisTb4b4b4)

Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8ffasBleScanStartExceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleScanStartException? Tb4b4b4{
Tff7b72var Te6edf3currentTb4b4b4: Te6edf3Throwable? Tff7b72= Tff7b72this
Tff7b72var Te6edf3depth Tff7b72= T79c0ff0
Tff7b72while Tb4b4b4(Te6edf3current Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3depth Tff7b72< Te6edf3MAX_SCAN_START_FAILURE_CAUSE_DEPTHTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3reason Tff7b72= Te6edf3currentTb4b4b4.Te6edf3scanStartFailureReasonOrNullTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3reason Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tff7b72return Te6edf3BleScanStartExceptionTb4b4b4(Te6edf3reasonTb4b4b4, Tff7b72thisTb4b4b4)
Te6edf3current Tff7b72= Te6edf3currentTb4b4b4.Te6edf3cause
Te6edf3depthTff7b72+Tff7b72+
Tb4b4b4}
Tff7b72return Tff7b72null
Tb4b4b4}

Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8ffscanStartFailureReasonOrNullTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleScanStartFailureReason? Tff7b72= Tff7b72when Tb4b4b4{
Te6edf3isApplicationRegistrationFailureTb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ApplicationRegistrationFailed
Te6edf3isScanningTooFrequentlyFailureTb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ScanningTooFrequently
Te6edf3isMissingScanPermissionTb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3MissingScanPermission
Tff7b72else Tff7b72-Tff7b72> Tff7b72null
Tb4b4b4}

T8b949e// Kable exposes Android scan-start registration failure as an IllegalStateException message,
T8b949e// so keep this matcher narrow and local to the scanner adapter.
Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8ffisApplicationRegistrationFailureTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Tff7b72this Tff7b72is Te6edf3IllegalStateException Tff7b72&Tff7b72&
Te6edf3messageTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3failureMessage Tff7b72-Tff7b72>
Te6edf3failureMessageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffapp cannot be registeredTa5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tff7b72|Tff7b72|
Te6edf3failureMessageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffSCAN_FAILED_APPLICATION_REGISTRATION_FAILEDTa5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4} Tff7b72=Tff7b72= Tff7b72true

Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8ffisScanningTooFrequentlyFailureTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Tff7b72this Tff7b72is Te6edf3IllegalStateException Tff7b72&Tff7b72&
Te6edf3messageTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3failureMessage Tff7b72-Tff7b72>
Te6edf3failureMessageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffscanning too frequentlyTa5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tff7b72|Tff7b72|
Te6edf3failureMessageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffSCAN_FAILED_SCANNING_TOO_FREQUENTLYTa5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4} Tff7b72=Tff7b72= Tff7b72true

T8b949e// Kable's scan-permission check throws a plain IllegalStateException "Missing required <permission> for scanning"
T8b949e// (ACCESS_COARSE/FINE_LOCATION on Android <12, BLUETOOTH_SCAN on 12+). Match the stable message anchors.
Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8ffisMissingScanPermissionTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Tff7b72this Tff7b72is Te6edf3IllegalStateException Tff7b72&Tff7b72&
Te6edf3messageTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3failureMessage Tff7b72-Tff7b72>
Te6edf3failureMessageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffMissing requiredTa5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tff7b72&Tff7b72&
Te6edf3failureMessageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fffor scanningTa5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4} Tff7b72=Tff7b72= Tff7b72true

Served by rngit 1.5.0 - Generated in 0.07s